﻿namespace TestApp.DataService
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using System.Reactive;
    using System.Reactive.Disposables;
    using System.Reactive.Linq;
    using Contracts;
    using JetBrains.Annotations;

    internal sealed class DataAggregator : IDataAggregator
    {
        private readonly object _lockObject = new object();
        private readonly IDataStore _dataStore;
        private readonly CompositeDisposable _anchors = new CompositeDisposable();
        private readonly IObservable<Unit> _hasNewEntitySource;
        [CanBeNull] private AggregateEntityDto[] _aggregateEntities;
        private long _curVersion;

        public DataAggregator(
            [NotNull] IDataProvider dataProvider,
            [NotNull] IDataStore dataStore)
        {
            Contract.Requires<ArgumentNullException>(dataProvider != null);
            Contract.Requires<ArgumentNullException>(dataStore != null);
            _dataStore = dataStore;
            var dataSource = dataProvider.CreateDataSource();
            _anchors.Add(dataSource.Subscribe(ProcessEntity));
            _hasNewEntitySource = dataSource.Select(i => Unit.Default).StartWith(Unit.Default);
        }

        public IObservable<Unit> CreateHasNewEntitySource()
        {
            Contract.Ensures(Contract.Result<IObservable<Unit>>() != null);
            return _hasNewEntitySource;
        }

        public bool TryGetEntities(long version, out long dataVersion, out IEnumerable<EntityDto> entities)
        {
            Contract.Requires<ArgumentOutOfRangeException>(version >= 0);
            IEnumerable<DataEntityDto> dataEntities;
            if (!_dataStore.TryGetDataEntities(version, out dataVersion, out dataEntities))
            {
                entities = null;
                return false;
            }

            entities = Array.AsReadOnly(dataEntities.Union(GetAggregates().Cast<EntityDto>()).ToArray());
            return true;
        }

        public void Dispose()
        {
            _anchors.Dispose();
        }

        private void ProcessEntity([NotNull] DataEntityDto entity)
        {
            Contract.Requires<ArgumentNullException>(entity != null);
            _dataStore.SaveDataEntity(entity);
        }

        [NotNull] private IEnumerable<AggregateEntityDto> GetAggregates()
        {
            Contract.Ensures(Contract.Result<IEnumerable<AggregateEntityDto>>() != null);
            long dataVersion;
            IEnumerable<DataEntityDto> dataEntities;
            if (!_dataStore.TryGetDataEntities(0, out dataVersion, out dataEntities))
            {
                return Enumerable.Empty<AggregateEntityDto>();
            }

            var curEntities = dataEntities.ToArray();
            lock (_lockObject)
            {
                if (_curVersion > dataVersion)
                {
                    return _aggregateEntities ?? Enumerable.Empty<AggregateEntityDto>();
                }

                var sumAggregate = new AggregateEntityDto(AggregateType.Sum);
                var maxAggregate = new AggregateEntityDto(AggregateType.Max);
                var minAggregate = new AggregateEntityDto(AggregateType.Min);
                var avgAggregate = new AggregateEntityDto(AggregateType.Avg);

                for (var paramIndex = 0; paramIndex < EntityDto.ParameterCount; paramIndex++)
                {
                    sumAggregate.Parameters[paramIndex] = curEntities.Sum(entityDto => entityDto.Parameters[paramIndex]);
                    maxAggregate.Parameters[paramIndex] = curEntities.Max(entityDto => entityDto.Parameters[paramIndex]);
                    minAggregate.Parameters[paramIndex] = curEntities.Min(entityDto => entityDto.Parameters[paramIndex]);
                    avgAggregate.Parameters[paramIndex] = curEntities.Average(entityDto => entityDto.Parameters[paramIndex]);
                }

                _aggregateEntities = new[] {sumAggregate, maxAggregate, minAggregate, avgAggregate};
                _curVersion = dataVersion;
            }

            return _aggregateEntities;
        }
    }
}